[net11.0] Expose safe area contract for custom views - #37750
Conversation
Make the per-edge safe area interfaces and shared bindable-property plumbing reusable outside MAUI. Keep platform inset reporting internal, migrate built-in controls, and add custom-view regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 37750Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 37750" |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR exposes public per-edge safe-area contracts for custom MAUI views and native hosts, adds shared plumbing, and preserves internal iOS inset reporting.
Changes:
- Publishes safe-area interfaces and edge lookup APIs.
- Migrates built-in controls to shared safe-area helpers.
- Adds API baselines and regression coverage.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Summary |
|---|---|
src/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/PublicAPI/net/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt |
Records Core API additions. |
src/Core/src/Primitives/SafeAreaEdges.cs |
Exposes per-edge lookup. |
src/Core/src/Platform/iOS/MauiView.cs |
Uses the internal inset sink. |
src/Core/src/Core/ISafeAreaView2.cs |
Publishes the per-edge safe-area contract. |
src/Core/src/Core/ISafeAreaInsets.cs |
Defines internal inset reporting. |
src/Core/src/Core/ISafeAreaElement.cs |
Publishes the shared element contract. |
src/Controls/tests/Core.UnitTests/SafeAreaTests.cs |
Critical (1 vote): the private CustomSafeAreaView has a non-public constructor, causing Activator.CreateInstance(Type) to throw MissingMethodException. |
src/Controls/src/Core/ScrollView/ScrollView.cs |
Migrates safe-area behavior. |
src/Controls/src/Core/SafeAreaElement.cs |
Adds shared safe-area property plumbing. |
src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt |
Records Controls API additions. |
src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt |
Records Controls API additions. |
src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt |
Records Controls API additions. |
src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt |
Records Controls API additions. |
src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt |
Records Controls API additions. |
src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt |
Records Controls API additions. |
src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt |
Records Controls API additions. |
src/Controls/src/Core/Page/Page.cs |
Implements inset handling. |
src/Controls/src/Core/Layout/Layout.cs |
Migrates safe-area behavior. |
src/Controls/src/Core/ContentView/ContentView.cs |
Migrates safe-area behavior. |
src/Controls/src/Core/ContentPage/ContentPage.cs |
Migrates safe-area behavior. |
src/Controls/src/Core/Border/Border.cs |
Migrates safe-area behavior. |
Suppressed comments (4)
src/Controls/src/Core/ContentPage/ContentPage.cs:174
HasExplicitSafeAreaEdgesnow correctly usesIsSafeAreaEdgesSet, but the adjacent per-edge resolver still usesIsSet.IsSettreats default-value creation as set, so merely readingSafeAreaEdgesfirst causes this method to skip the iOSIgnoreSafeAreafallback and return the defaultNoneinstead. UseSafeAreaElement.IsSafeAreaEdgesSet(this)here as well and cover the read-then-resolve sequence.
bool ISafeAreaView2.HasExplicitSafeAreaEdges => SafeAreaElement.IsSafeAreaEdgesSet(this);
src/Controls/src/Core/Layout/Layout.cs:376
- Please regenerate the profiled AOT artifacts for this rename. The checked-in
maui.aotprofile.txtandmaui-sc.aotprofile.txtstill listLayout/ScrollView:ISafeAreaElement.SafeAreaEdgesDefaultValueCreator, while these implementations are nowGetDefaultSafeAreaEdges;Microsoft.Maui.Controls.targetsimports the corresponding binary profiles for Android, so these calls will no longer match the profiled methods (and the profile tool may report missing methods).
SafeAreaEdges ISafeAreaElement.GetDefaultSafeAreaEdges()
{
return SafeAreaEdges.Container;
}
src/Controls/src/Core/ScrollView/ScrollView.cs:553
- This renames the explicit
ISafeAreaElementimplementation used byLayoutandScrollView, but the checked-in profiled-AOT lists still referenceMicrosoft.Maui.ISafeAreaElement.SafeAreaEdgesDefaultValueCreator(inmaui.aotprofile.txtandmaui-sc.aotprofile.txt). Regenerate those profile outputs so they referenceGetDefaultSafeAreaEdges; otherwise the profiles are stale and no longer describe methods in the assembly.
SafeAreaEdges ISafeAreaElement.GetDefaultSafeAreaEdges()
src/Controls/tests/Core.UnitTests/SafeAreaTests.cs:287
- These assertions verify the public contracts and property mapping only; they never create a handler or send insets through the iOS
MauiView/Android inset-listener paths. A regression in the platform-sideISafeAreaView2lookup or listener refresh would therefore still pass these tests even though a direct customViewno longer receives the advertised per-edge behavior. Add focused device coverage for the custom view on Android and iOS, or include the probe as an automated test.
public void CustomView_CanReuseSafeAreaEdgesContract()
{
var view = new CustomSafeAreaView();
var safeAreaView = (ISafeAreaView2)view;
|
Validated the review findings against the current head:
This branch is actively owned in another worktree with recent source/build activity, so the monitor is reply-only here: I did not edit or push, and the actionable thread remains open for the owning agent. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
|
@copilot-pull-request-reviewer addressed the validated feedback in ecf8393: corrected ContentPage explicit-edge detection after default-value reads, regenerated both binary/text AOT profiles for the renamed explicit implementations, fixed private test-view construction, and added Android/iOS custom-view handler coverage. Focused validation passed (64 unit, 40 iOS View, 5 iOS Page, and 51 Android View tests). This is ready for re-review — thanks! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
src/Controls/src/Core/SafeAreaElement.cs:24
- This newly reusable property is not wired to safe-area invalidation on Mac Catalyst: the existing
ViewHandlermapper is guarded by#if ANDROID || IOS, whileMauiViewandViewHandler.iOS.csalso compile for Mac Catalyst. Consequently, changing a custom view'sSafeAreaEdgesafter its handler is connected does not invalidate the safe-area layout there. RegisterMapSafeAreaEdgesforMACCATALYSTas well and add a post-handler-change regression test.
public static readonly BindableProperty SafeAreaEdgesProperty =
BindableProperty.Create(nameof(ISafeAreaElement.SafeAreaEdges), typeof(SafeAreaEdges), typeof(ISafeAreaElement), SafeAreaEdges.Default,
defaultValueCreator: SafeAreaEdgesDefaultValueCreator);
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
|
@copilot-pull-request-reviewer[bot] addressed both latest findings in dc737cb: descendant safe-area caches are invalidated when an ancestor strategy changes, and the mapper now runs on Mac Catalyst. Focused View device tests passed 41/41 on both iOS and Mac Catalyst. This is ready for re-review — thanks! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 36 out of 38 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txt:3287
- The text snapshot now records
GetDefaultSafeAreaEdges, but the pairedmaui-sc.aotprofilebinary is not regenerated.Microsoft.Maui.Controls.targetsimports that binary for Android profiled AOT, so consumers will still ship a profile containing the removed interface method and will not profile the new calls. Please rerun the documentedRecordtarget formaui-scand commit the regenerated binary with this snapshot.
src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui.aotprofile.txt:2594 - The text snapshot now records
GetDefaultSafeAreaEdges, but the pairedmaui.aotprofilebinary is not regenerated.Microsoft.Maui.Controls.targetsimports that binary for Android profiled AOT, so consumers will still ship a profile containing the removed interface method and will not profile the new calls. Please rerun the documentedRecordtarget formauiand commit the regenerated binary with this snapshot.
Microsoft.Maui.SafeAreaEdges Microsoft.Maui.Controls.Layout:Microsoft.Maui.ISafeAreaElement.GetDefaultSafeAreaEdges ()
|
Thanks for the re-review. The two suppressed AOT-profile notes are already satisfied by ecf8393: that commit changed both |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Additional exact-head hardening is pushed at The keyboard-overlap cache now compares absolute values after device-pixel rounding. The prior adjacent-delta comparison could update its baseline after every sub-pixel move and therefore miss a sequence whose cumulative motion crossed a physical pixel boundary. The strengthened cache regression first proves an unchanged/sub-pixel pass performs zero ancestor reads, then advances by two |
This comment has been minimized.
This comment has been minimized.
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial review
An independent reviewer process reviewed the full GitHub-scoped diff and exact source at 104f8146610f38465118de78bdd2c2f9336e4751.
Finding
One exact-diff iOS regression survived adversarial consensus: a floating or undocked keyboard can produce false SoftInput bottom padding for a laterally disjoint view. See the inline comment for the concrete path and fix. Consensus: 2/3 reviewers after dispute.
Prior review reconciliation
The wrong secondary-display scale and full-subtree invalidation costs were independently rediscovered, but MauiBot already documented them in issue comment 5420344625; they are not duplicated here. Earlier findings about coordinate conversion, active ancestors, descendant convergence, explicit defaults, and specificity propagation are addressed in the current code.
Finalization
The title accurately describes the new public Safe Area contract, and the detailed description matches the implementation, compatibility model, API baselines, AOT profiles, and test scope.
Methodology
3 independent reviewers with adversarial consensus + a separate MAUI domain specialist. Review event: COMMENT; no approval or change request is implied.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 5 findings
See inline comments for details.
Handle floating keyboard geometry, preserve UIKit-adjusted scroll insets, and avoid redundant ancestor work while retaining layout-order correctness. Add focused device and specificity regressions for every exact-head review finding. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
This comment has been minimized.
This comment has been minimized.
|
The AI summary is based on stale head |
This comment has been minimized.
This comment has been minimized.
|
/azp run maui-pr |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Clarify the intentional UIKit and keyboard semantics, document compatibility-resolved effective values, and remove unrelated Android event-hook syntax churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
Explain why interface specificity is required, why native subtree invalidation cannot stop at intermediate MAUI views, how nested keyboard residuals settle, and why the Page strategy supports custom subclasses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: de9c7c01-82c4-42fd-9ab7-882279aadd15
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 12 findings
See inline comments for details.
|
|
||
| var oldApplyingSafeAreaAdjustments = _appliesSafeAreaAdjustments; | ||
| _appliesSafeAreaAdjustments = !IsParentHandlingSafeArea() && RespondsToSafeArea() && !_safeArea.IsEmpty; | ||
| _appliesSafeAreaAdjustments = RespondsToSafeArea() && !_safeArea.IsEmpty; |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
❌ Error — [major] Layout Measure-Arrange / Regression Prevention: the !IsParentHandlingSafeArea() term was dropped from _appliesSafeAreaAdjustments, but ancestor suppression was only re-added to the first branch above (SystemAdjustedContentInset == Zero || ContentInsetAdjustmentBehavior == Never, where ExcludeParentHandledSafeAreaEdges is applied). In the else branch _safeArea = SystemAdjustedContentInset.ToSafeAreaInsets() is assigned with no parent filtering at all, so _appliesSafeAreaAdjustments is now true whenever UIKit reports a non-zero adjusted inset — even when an ancestor MauiView is already padding the same edges.
Concrete scenario: ContentPage SafeAreaEdges="All" (or Container) → VerticalStackLayout → ScrollView whose content is taller than the viewport (so ContentInsetAdjustmentBehavior stays Automatic/Always and SystemAdjustedContentInset != Zero). Before this change the parent-handling check forced _appliesSafeAreaAdjustments = false for that scroll view; now both the ancestor MauiView and the MauiScrollView inset the same top/bottom edges. This is exactly the double-apply/oscillation shape tracked by #33595 and #32586, and the ExcludeParentHandledSafeAreaEdges guard that replaces it is unreachable on this branch. Either apply the same ancestor-edge exclusion to the SystemAdjustedContentInset result, or restore an IsParentHandlingSafeArea-style gate for that branch. ScrollViewHandlerTests.iOS.cs only adds LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge, which exercises the Never branch, so this path has no regression coverage.
There was a problem hiding this comment.
Thanks. I am keeping UIKit-adjusted insets unsuppressed: AdjustedContentInset is already physically applied by UIKit, and _safeArea compensates the MAUI viewport for that native inset; filtering it through ancestor padding discards part of the real scroll viewport. SystemAdjustedScrollViewInsetsAreNotSuppressedByParent covers this exact branch, and the inverse suppression mutation was the sole failure in the 62-test iOS View suite. The manually computed/Never branch still applies per-edge ancestor suppression.
| // depend on whether the ancestor has already completed its layout pass. | ||
| // Keyboard overlap only changes Bottom, so farther ancestors can omit that | ||
| // calculation after the nearest nonzero Bottom has been classified. | ||
| var safeArea = mauiView.GetAdjustedSafeAreaInsets( |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
GetParentHandledSafeAreaEdges now recomputes each ancestor's full adjusted safe area (mauiView.GetAdjustedSafeAreaInsets(...)) while walking the superview chain. The previous implementation read the already-computed mv._appliesSafeAreaAdjustments bool field. Each call now performs SafeAreaViewStrategy.TryGetSafeAreaEdges (interface type tests + a virtual GetDefaultSafeAreaEdges() call + SafeAreaEdges struct construction), a 4-iteration region scan, and — for the nearest ancestor with a zero bottom — TryGetSoftInputBottomOverlap with ConvertRectToView and ConvertRectFromCoordinateSpace.
This runs per MauiView per layout pass, making the cost O(views × depth). It is cached in _parentHandledSafeAreaEdges, but the new InvalidateSafeArea(UIView) / InvalidateDescendantSafeAreas() recursion (lines 863 and 976) nulls that cache for every descendant on each safe-area change, so the cache is discarded exactly when the tree is being re-laid-out. On a page containing a CollectionView with many realized MauiView-backed cells this is a measurable per-keyboard-event and per-rotation regression. Please attach a dotnet-trace comparison against the base commit, or hoist the ancestor computation so a single upward walk populates all descendants.
There was a problem hiding this comment.
Thanks. I am keeping the input-based ancestor lookup because reading an ancestor cached applied-state flag makes child suppression depend on ancestor layout order; ParentSafeAreaSuppressionDoesNotDependOnLayoutOrder covers that regression. The result is cached per descendant until a real invalidation, empty insets skip the lookup, the walk stops when all four edges resolve, and keyboard geometry is omitted for farther ancestors once Bottom resolves. Those bounds have focused device coverage; there is no measured regression supporting a correctness-reducing cache.
| /// <summary> | ||
| /// Invalidates safe area state for a native subtree. | ||
| /// </summary> | ||
| internal static void InvalidateSafeArea(UIView platformView) |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
InvalidateSafeArea(UIView) recurses into the entire native subtree unconditionally, with no depth bound and no early exit for branches that contain no safe-area-aware views. Every node access of platformView.Subviews marshals a fresh managed UIView[] out of the underlying NSArray, so a page with N native descendants allocates N arrays per invalidation.
This is invoked from ClearKeyboardState(), KeyboardWillShow, KeyboardWillChangeFrame, and ViewHandler.iOS.MapSafeAreaEdges — i.e. on every keyboard show/hide/frame change. With a CollectionView of realized cells (each cell being a MauiView subtree), one keyboard toggle now walks and allocates across all of them. Consider bounding the walk (stop descending once a subtree contains no MauiView/MauiScrollView), or reusing a cached descendant list.
There was a problem hiding this comment.
Thanks. The complete native-subtree walk is intentional because handler-backed descendants can sit below arbitrary UIKit wrappers, so there is no sound type-based signal that a branch contains no MauiView/MauiScrollView. A farther Top-handling ancestor can still affect a grandchild below an intermediate Bottom-handling view. Stopping below that intermediate view was mutation-tested and failed only ChangingAncestorSafeAreaEdgesInvalidatesEdgeDisjointGrandchild; restoring the walk passed. Connect-time mapping already skips this traversal, and runtime broadcasts are required for keyboard/property invalidations.
| (oldSafeArea.EqualsAtPixelLevel(_safeArea) || !_appliesSafeAreaAdjustments); | ||
|
|
||
| if (!interactionUnchanged) | ||
| InvalidateDescendantSafeAreas(); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
InvalidateDescendantSafeAreas() is called from inside ValidateSafeArea(), which itself runs on the layout path (AdjustForSafeArea/LayoutSubviews). Calling SetNeedsLayout() on every descendant during a layout pass schedules another full pass, and each descendant that changes then schedules a pass for its descendants.
Combined with the intentional two-phase keyboard convergence introduced at line 592 (child keeps its own positive overlap until the parent arranges it above the keyboard), a single keyboard show on a SoftInput-bottom page containing a SoftInput-bottom child costs at minimum: pass 1 (parent pads, child also pads) → pass 2 (child overlap now 0, child suppresses) → pass 3 (parent re-measures the shrunken child). If the child's arranged position depends on the inset it applies — e.g. a VerticalStackLayout with SafeAreaEdges="SoftInput" inside a ScrollView whose content height feeds back into the parent's arrange — the overlap never reaches a fixed point and the passes oscillate. The EqualsAtPixelLevel guard only terminates the loop when the values converge; it does not prevent a non-converging feedback cycle. Please confirm with a device test that measures layout-pass count across a keyboard show for nested SoftInput edges.
There was a problem hiding this comment.
Thanks. InvalidateDescendantSafeAreas runs only when the parent safe-area interaction actually changes, not on every validation/layout pass, and UIKit coalesces SetNeedsLayout. NestedKeyboardSafeAreasUseCrossPlatformArrange exercises the real MAUI Grid measure/arrange path, while ParentAndChildKeyboardSafeAreasDoNotDoublePadArrangedChild and ParentAndChildKeyboardSafeAreasProtectOverflowingChild cover both convergence outcomes. There is no reproducing oscillation here, and a scheduler-specific pass-count assertion would be brittle, so I am keeping the behavior.
| parentHandledEdges.Left != SafeAreaRegions.None ? 0 : safeArea.Left, | ||
| parentHandledEdges.Right != SafeAreaRegions.None ? 0 : safeArea.Right, | ||
| parentHandledEdges.Top != SafeAreaRegions.None ? 0 : safeArea.Top, | ||
| !bottomIncludesKeyboardOverlap && parentHandledEdges.Bottom != SafeAreaRegions.None ? 0 : safeArea.Bottom); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
ExcludeParentHandledSafeAreaEdges skips bottom-edge suppression whenever bottomIncludesKeyboardOverlap is true, which removes the !IsSoftInputHandledByParent(this) gate that previously prevented an ancestor and a descendant from both padding for the keyboard. The comment states the child's overlap becomes zero "once its parent arranges that child above the keyboard" — but that assumption fails when the parent's inset does not move the child.
Concrete scenario: a Grid page with SafeAreaEdges="SoftInput" whose bottom row child also sets SafeAreaEdges="SoftInput" and is VerticalOptions="End" inside a fixed-height row, or a child inside an absolutely-positioned container. The parent's bottom padding shrinks the parent's content rect but the child's window-space bottom is unchanged, so TryGetSoftInputBottomOverlap keeps returning a positive overlap forever and the keyboard padding is applied twice (content pushed up by roughly 2× the keyboard height). At minimum this needs a device test covering a nested SoftInput child whose frame is not repositioned by the parent's inset.
There was a problem hiding this comment.
That fixed-position case is the reason the positive residual is retained. If parent padding does not move the child, the parent has not protected that child from the keyboard; suppressing the child overlap would under-pad it, not prevent a duplicate movement. The suite separately proves that an arranged child reaches zero residual and does not double-pad, while an overflowing/fixed child keeps only its own frame-relative overlap. Restoring the old parent SoftInput gate fails the overflowing-child regression, so I am keeping this logic.
|
|
||
| if (handler.PlatformView is PlatformView platformView) | ||
| { | ||
| MauiView.InvalidateSafeArea(platformView); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
MapSafeAreaEdges now performs a full recursive native-subtree invalidation (MauiView.InvalidateSafeArea(platformView)) on every mapper invocation. Because SafeAreaElement.SafeAreaEdgesProperty sets UpdateHandlerOnSpecificityChange = true (SafeAreaElement.cs:36) and Element.OnBindablePropertySet now forwards specificity-only transitions as changed: true (Element.cs:711), this mapper also fires when the value is unchanged and only the setter specificity moved — e.g. an implicit Style re-applying SafeAreaEdges="None" over the created default, or a VisualState re-entering the same state.
The result is a whole-subtree walk (including realized CollectionView cells) for a no-op value change. Consider comparing the newly resolved effective edges against the previously applied ones and returning early when they are pixel-identical, or scoping the invalidation to the handler's own platform view plus safe-area-aware descendants.
There was a problem hiding this comment.
A specificity-only transition is semantically meaningful here: the raw SafeAreaEdges value can stay equal while HasExplicitSafeAreaEdges changes and therefore changes the resolved default strategy. SafeAreaEdgesSpecificityChangesUpdateHandlerOnlyOnce verifies that a transition updates exactly once and a repeated same-specificity assignment does not update. Caching only this handler applied value would also miss descendants whose ancestor-suppression result changes, which is why the correctness-required descendant invalidation remains.
| /// The type must use <see cref="SafeAreaEdgesProperty"/> as the backing store for its safe area property. | ||
| /// </remarks> | ||
| /// <exception cref="ArgumentNullException">Thrown when <paramref name="bindable"/> is <see langword="null"/>.</exception> | ||
| public static bool IsSafeAreaEdgesSet(BindableObject bindable) |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
💡 Suggestion — [minor] Public API Surface: IsSafeAreaEdgesSet(BindableObject) is now public but silently returns false for any BindableObject that declares its own SafeAreaEdges bindable property instead of reusing this shared SafeAreaEdgesProperty instance. The remark documents the requirement, but the failure mode is silent: a custom control that follows the ISafeAreaElement guidance yet declares BindableProperty.Create(nameof(SafeAreaEdges), ...) locally will report "never explicitly set", so SafeAreaViewStrategy resolves it via GetDefaultSafeAreaEdges() and the user's assigned value is ignored on platforms that branch on explicitness.
Given this ships as a public extensibility contract, consider making the mismatch detectable — e.g. having IsSafeAreaEdgesSet throw (or Debug.Assert) when bindable.GetType() does not expose SafeAreaElement.SafeAreaEdgesProperty as its SafeAreaEdgesProperty — rather than silently degrading.
There was a problem hiding this comment.
The helper contract deliberately requires reuse of the shared property, as its remarks and the interface guidance state. Reflection over a public static field would be trimming-sensitive and still could not prove which bindable property the instance accessor actually uses. A custom implementation with its own property must compute HasExplicitSafeAreaEdges itself; implementations choosing the shared helper get one canonical identity, and the shared default creator already throws when used by a non-ISafeAreaElement. I am keeping that explicit, predictable contract.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@kubaflo — new AI review results are available based on commit
92f72f9.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ⚠️ INCONCLUSIVE
Platform: IOS · Base: net11.0 · Merge base: ec79089f
🩺 Could not verify — environment/infrastructure error. The gate ran the tests but hit an environment error (an emulator/simulator/Appium/XHarness flake, a device that would not boot, or an empty/invalid result file), so it could not record a real pass/fail. The /review to retry on a fresh agent.
XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.ScrollViewHandlerTests' (the target tests did not run).
⚠️ Gate coverage limitations
- The A/B gate did not verify 1 dropped DeviceTest group(s): ViewTests (CustomViewSafeAreaEdgesReachMauiView, ChangingParentSafeAreaEdgesInvalidatesDescendants, MeasureInvalidatedParentDoesNotBlockDescendantSafeAreaInvalidation, ParentSafeAreaSuppressionDoesNotDependOnLayoutOrder, ResolvedBottomEdgeSkipsFartherAncestorKeyboardGeometry, ResidualParentInsetDoesNotSuppressChildSafeArea, ParentHandledEdgeLookupStopsWhenAllEdgesAreResolved, EmptySafeAreaSkipsParentHandledEdgeLookup, EmptyManualScrollViewSafeAreaSkipsParentHandledEdgeLookup, KeyboardSafeAreaChangesInvalidateDescendants, ParentContainerSafeAreaDoesNotSuppressChildKeyboardSafeArea, ParentAndChildKeyboardSafeAreasDoNotDoublePadArrangedChild, ParentAndChildKeyboardSafeAreasProtectOverflowingChild, HiddenKeyboardSkipsDuplicateSoftInputStrategyResolution, UnchangedKeyboardGeometryKeepsAncestorSafeAreaCache, ChangedKeyboardOverlapInvalidatesNonSoftInputDescendants, SoftInputAncestorInsideScrollViewDoesNotSuppressKeyboardAutoScroll, KeyboardFrameConvertsFromScreenToWindowCoordinates, FloatingKeyboardUsesClampedViewIntersection, NestedKeyboardSafeAreasUseCrossPlatformArrange, ParentOnlySuppressesOverlappingChildSafeAreaEdges, ParentOnlySuppressesOverlappingScrollViewSafeAreaEdges, SystemAdjustedScrollViewInsetsAreNotSuppressedByParent, ChangingAncestorSafeAreaEdgesInvalidatesEdgeDisjointGrandchild). Deep UI Tests runs HostApp UI categories only and does not execute DeviceTests; separate device-test validation is required.
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 SafeAreaTests SafeAreaTests |
🛠️ BUILD ERROR | ✅ PASS — 15s |
📄 SafeAreaEdgesTests SafeAreaEdgesTests |
🛠️ BUILD ERROR | ✅ PASS — 19s |
📄 Tests Tests |
🛠️ BUILD ERROR | ✅ PASS — 120s |
📱 PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback) Category=Page |
🛠️ BUILD ERROR | |
📱 ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge) Category=ScrollView |
🔴 Without fix — 🧪 SafeAreaTests: 🛠️ BUILD ERROR · 15s
Error-relevant lines (filtered from the build log):
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(577,35): error CS0539: 'SafeAreaTests.DerivedSafeAreaContentPage.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(583,26): error CS0539: 'SafeAreaTests.DerivedDefaultSafeAreaContentPage.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(584,35): error CS0539: 'SafeAreaTests.DerivedDefaultSafeAreaContentPage.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(597,26): error CS0539: 'SafeAreaTests.CustomNoneSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(599,35): error CS0539: 'SafeAreaTests.CustomNoneSafeAreaView.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(612,26): error CS0539: 'SafeAreaTests.CustomMixedSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(614,35): error CS0539: 'SafeAreaTests.CustomMixedSafeAreaView.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(497,43): error CS0535: 'SafeAreaTests.CustomSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(524,43): error CS0535: 'SafeAreaTests.CustomSafeAreaPage' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(587,47): error CS0535: 'SafeAreaTests.CustomNoneSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(602,48): error CS0535: 'SafeAreaTests.CustomMixedSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj]
🟢 With fix — 🧪 SafeAreaTests: PASS ✅ · 15s
(no coded error found; showing last 1200 chars)
Default [< 1 ms]
Passed SafeAreaEdgesTypeConverter_ConvertFromFourValues [< 1 ms]
Passed IsSafeAreaEdgesSet_NullBindableThrows [< 1 ms]
Passed SafeAreaEdgesTypeConverter_ConvertFromInvalidValue_ThrowsException [< 1 ms]
Passed SafeAreaEdges_UniformConstructor_AppliesAllEdges [< 1 ms]
Passed StackLayouts_RespectUserSettings [< 1 ms]
Passed GetEdgeValue_TwoValues_AppliesCorrectly [< 1 ms]
Passed CustomView_DefaultRegionsUseDeclaredEdges [< 1 ms]
Passed GetEdgeValue_FourValues_AppliesCorrectly [< 1 ms]
Passed SafeAreaEdgesTypeConverter_ConvertFromInvalidLength_ThrowsException [< 1 ms]
Passed Layout_ImplementsISafeAreaView [< 1 ms]
Passed SafeAreaEdges_AllEnumValues_WorkCorrectly [< 1 ms]
[xUnit.net 00:00:00.67] Finished: Microsoft.Maui.Controls.Core.UnitTests
Passed CustomPage_CanOverrideInheritedSafeAreaStrategy [< 1 ms]
Passed CustomView_CanReuseSafeAreaEdgesContract [< 1 ms]
Passed StackLayout_HorizontalOrientation_RespectsDirectProperty_RTL [< 1 ms]
Passed GetEdges_DefaultValue_ReturnsDefault [< 1 ms]
Passed HasExplicitSafeAreaEdges_StyleValueCountsAsExplicit [1 ms]
Test Run Successful.
Total tests: 77
Passed: 77
Total time: 0.8697 Seconds
🔴 Without fix — 📄 SafeAreaEdgesTests: 🛠️ BUILD ERROR · 10s
Error-relevant lines (filtered from the build log):
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(93,34): error CS0539: 'CustomSafeAreaElement.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(81,52): error CS0535: 'CustomSafeAreaElement' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
🟢 With fix — 📄 SafeAreaEdgesTests: PASS ✅ · 19s
(no coded error found; showing last 1200 chars)
aui.Controls.Xaml.UnitTests
[xUnit.net 00:00:02.95] Starting: Microsoft.Maui.Controls.Xaml.UnitTests
Passed FourValueConversions(inflator: SourceGen) [26 ms]
Passed FourValueConversions(inflator: XamlC) [< 1 ms]
Passed FourValueConversions(inflator: Runtime) [24 ms]
Passed SingleValueConversions(inflator: XamlC) [< 1 ms]
Passed SingleValueConversions(inflator: SourceGen) [< 1 ms]
Passed SingleValueConversions(inflator: Runtime) [1 ms]
Passed TwoValueConversions(inflator: XamlC) [< 1 ms]
Passed TwoValueConversions(inflator: Runtime) [1 ms]
Passed TwoValueConversions(inflator: SourceGen) [< 1 ms]
[xUnit.net 00:00:03.05] Finished: Microsoft.Maui.Controls.Xaml.UnitTests
Passed PropertyInflation_WorksWithAllEnumValues(inflator: XamlC) [1 ms]
Passed PropertyInflation_WorksWithAllEnumValues(inflator: Runtime) [1 ms]
Passed PropertyInflation_WorksWithAllEnumValues(inflator: SourceGen) [< 1 ms]
Passed ControlSpecificProperties(inflator: Runtime) [4 ms]
Passed ControlSpecificProperties(inflator: XamlC) [< 1 ms]
Passed ControlSpecificProperties(inflator: SourceGen) [< 1 ms]
Test Run Successful.
Total tests: 15
Passed: 15
Total time: 3.2648 Seconds
🔴 Without fix — 📄 Tests: 🛠️ BUILD ERROR · 7s
Error-relevant lines (filtered from the build log):
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(93,34): error CS0539: 'CustomSafeAreaElement.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(81,52): error CS0535: 'CustomSafeAreaElement' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj]
🟢 With fix — 📄 Tests: PASS ✅ · 120s
(no coded error found; showing last 1200 chars)
or(targetPlatformIdentifier: "") [1 s]
Passed SingleProject_NonPlatformBuildExcludesPlatformSpecificFoldersButKeepsSharedFolder [1 s]
Passed RandomEmbeddedResource [1 s]
Passed SingleProject_RecognizedTfmIgnoresNeutralBackendSelector [1 s]
Passed TargetsShouldSkip [2 s]
[xUnit.net 00:01:53.69] TouchXamlFile [SKIP]
[xUnit.net 00:01:53.69] source gen changes
Skipped TouchXamlFile [1 ms]
Passed ItemDisplayBindingWithoutDataTypeFails(inflator: XamlC) [87 ms]
Passed ItemDisplayBindingWithoutDataTypeFails(inflator: SourceGen) [4 ms]
Passed ItemDisplayBindingWithoutDataTypeFails(inflator: Runtime) [3 ms]
Passed RequiredFieldsAndPropertiesAreSet(inflator: XamlC) [< 1 ms]
Passed RequiredFieldsAndPropertiesAreSet(inflator: SourceGen) [17 ms]
Passed RequiredFieldsAndPropertiesAreSet(inflator: Runtime) [< 1 ms]
Passed ThrowsOnMismatchingType(inflator: SourceGen) [6 ms]
Passed ThrowsOnMismatchingType(inflator: XamlC) [39 ms]
[xUnit.net 00:01:53.85] Finished: Microsoft.Maui.Controls.Xaml.UnitTests
Passed ThrowsOnMismatchingType(inflator: Runtime) [< 1 ms]
Test Run Successful.
Total tests: 2123
Passed: 2115
Skipped: 8
Total time: 1.9021 Minutes
🔴 Without fix — 📱 PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback): 🛠️ BUILD ERROR · 31s
Error-relevant lines (filtered from the build log):
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(93,26): error CS0539: 'ViewTests.CustomSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-ios]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(95,35): error CS0539: 'ViewTests.CustomSafeAreaView.GetDefaultSafeAreaEdges()' in explicit interface declaration is not found among members of the interface that can be implemented [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-ios]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(73,43): error CS0535: 'ViewTests.CustomSafeAreaView' does not implement interface member 'ISafeAreaElement.SafeAreaEdgesDefaultValueCreator()' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net11.0-ios]
Build FAILED.
🟢 With fix — 📱 PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback): ⚠️ ENV ERROR · 53s
No log file found
🔴 Without fix — 📱 ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge): ⚠️ ENV ERROR · 51s
No log file found
🟢 With fix — 📱 ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge): ⚠️ ENV ERROR · 52s
No log file found
⚠️ Failure Details (7 tests)
- 🛠️ SafeAreaTests without fix: build failed before tests could run
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Core.UnitTests/SafeAreaTests.cs(519,26): error CS0539: 'SafeAreaTests.CustomSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration i...
- 🛠️ SafeAreaEdgesTests without fix: build failed before tests could run
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is ...
- 🛠️ Tests without fix: build failed before tests could run
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/Xaml.UnitTests/SafeAreaEdgesTests.xaml.cs(91,25): error CS0539: 'CustomSafeAreaElement.HasExplicitSafeAreaEdges' in explicit interface declaration is ...
- 🛠️ PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback) without fix: build failed before tests could run
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/View/ViewTests.cs(93,26): error CS0539: 'ViewTests.CustomSafeAreaView.HasExplicitSafeAreaEdges' in explicit interface declaration...
⚠️ ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge) without fix:XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.ScrollViewHandlerTests' (the target tests did not run).⚠️ PageTests (ReadingDefaultSafeAreaEdgesPreservesLegacySafeAreaFallback) with fix:XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.PageTests' (the target tests did not run).⚠️ ScrollViewHandlerTests (LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge) with fix:XHarness did not produce the expected fresh result 'testResults.xml' for requested class(es) 'Microsoft.Maui.DeviceTests.ScrollViewHandlerTests' (the target tests did not run).
📁 Fix files reverted (43 files)
src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-blazor.aotprofilesrc/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-blazor.aotprofile.txtsrc/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofilesrc/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui-sc.aotprofile.txtsrc/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui.aotprofilesrc/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/maui.aotprofile.txtsrc/Controls/src/Core/BindableObject.cssrc/Controls/src/Core/BindableProperty.cssrc/Controls/src/Core/Border/Border.cssrc/Controls/src/Core/ContentPage/ContentPage.cssrc/Controls/src/Core/ContentView/ContentView.cssrc/Controls/src/Core/Element/Element.cssrc/Controls/src/Core/InputView/InputView.cssrc/Controls/src/Core/Layout/Layout.cssrc/Controls/src/Core/Page/Page.cssrc/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txtsrc/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txtsrc/Controls/src/Core/SafeAreaElement.cssrc/Controls/src/Core/ScrollView/ScrollView.cssrc/Core/src/Core/ISafeAreaElement.cssrc/Core/src/Core/ISafeAreaView2.cssrc/Core/src/Handlers/View/ViewHandler.Android.cssrc/Core/src/Handlers/View/ViewHandler.cssrc/Core/src/Handlers/View/ViewHandler.iOS.cssrc/Core/src/Platform/Android/MauiWindowInsetListener.cssrc/Core/src/Platform/Android/SafeAreaExtensions.cssrc/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cssrc/Core/src/Platform/iOS/MauiScrollView.cssrc/Core/src/Platform/iOS/MauiView.cssrc/Core/src/Platform/iOS/SafeAreaPadding.cssrc/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/net/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/netstandard/PublicAPI.Unshipped.txtsrc/Core/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
New files (not reverted):
src/Core/src/Core/ISafeAreaInsets.cssrc/Core/src/Core/ISafeAreaViewStrategy.cs
📋 Pre-Flight — Context & Validation
PR #37750 Pre-Flight
Context
- Title:
[net11.0] Expose safe area contract for custom views - Base:
net11.0 - Materialized review commit:
d7905a8af8829a1a8da9fb8992aa9175551724ac - Related issue: #37384, which reports that custom views cannot participate in the .NET 10 per-edge
SafeAreaEdgesmodel or expose an effective strategy to native hosts. - Gate: Inconclusive because the existing test could not be built or run. This is not evidence that the PR fix fails, and gate verification must not be rerun in STEP 5a.
Current PR Approach
The PR replaces the internal numbered ISafeAreaView2 contract with a public ISafeAreaElement contract, adds explicit/default-value metadata and a public effective-strategy resolver, exposes reusable Controls bindable-property plumbing, and routes Apple/Android safe-area handling through a shared internal strategy resolver. It retains the shipped ISafeAreaView fallback and adds substantial unit, XAML, device, HostApp, AOT-profile, and platform behavior coverage.
The materialized diff changes 56 files (3020 insertions, 530 deletions), including:
- public API and Controls property plumbing (
ISafeAreaElement,SafeAreaElement, API baselines); - an internal compatibility resolver (
ISafeAreaViewStrategy/SafeAreaViewStrategy); - iOS
MauiView,MauiScrollView, keyboard, inset, and handler paths; - Android listener and inset paths;
- built-in control defaults and bindable-property specificity behavior;
- safe-area unit, XAML, Controls device, Core device, and AOT-profile coverage.
The diff adds ISafeAreaInsets.cs and ISafeAreaViewStrategy.cs and deletes ISafeAreaView2.cs; candidate attempts must obey the try-fix baseline allow-list and report Blocked if the baseline state contains any NewFiles.
Alternative-Fix Requirement
Each candidate must use a distinct root-cause hypothesis and must not reproduce the PR's shared public-contract-plus-central-resolver design. Candidate 2 must also avoid candidate 1's recorded mechanism.
Test Contract
Primary test:
pwsh .github/scripts/BuildAndRunHostApp.ps1 -Platform ios -TestFilter "SafeAreaEdges"Only after the primary test passes, run every regression command supplied by the STEP 5a request, including repeated entries. Any regression failure makes the candidate Fail. A missing simulator/device is Blocked; build, compile, or script errors are Fail.
Each attempt allows one implementation/test pass and at most one focused correction/retest. The gate test must not be rerun, no full suite may be run, and the exact restore command is:
pwsh .github/scripts/EstablishBrokenBaseline.ps1 -RestoreWorkspace Constraints
The checkout already contains numerous unrelated modified/untracked CI-script files. They predate STEP 5a and must not be edited, removed, restored, stashed, or included in candidate diffs. gate/content.md is owned by the prior gate phase and must not be created or overwritten.
🔬 Code Review — Deep Analysis
Expert PR Evaluation — dotnet/maui #37750
Scope reviewed: local raw submitted commit d7905a8af8829a1a8da9fb8992aa9175551724ac against base ec79089f65, in read-only worktree /Users/cloudtest/vss/_work/1/s. Authoritative diff = git diff ec79089f65..d7905a8af8. The public PR head may have advanced remotely; that newer head was not reviewed or fetched. Unrelated dirty .github/eng files in the worktree were ignored.
Method: independence-first. The full diff and the relevant full files at d7905a8af8 (plus callers/consumers and prior behaviour at ec79089f65) were read before any PR narrative was consulted.
1. What the PR actually does (independent reading)
This is a safe-area contract refactor that converts an internal, page-centric interface into a public, per-edge extensibility contract, plus several behavioural changes to iOS safe-area/keyboard geometry.
Structural changes:
| Before | After |
|---|---|
internal interface ISafeAreaView2 (HasExplicitSafeAreaEdges, SafeAreaInsets setter, GetSafeAreaRegionsForEdge(int)) |
Deleted. Split three ways. |
internal interface ISafeAreaElement (SafeAreaEdges, SafeAreaEdgesDefaultValueCreator()) |
Now public, with SafeAreaEdges, HasExplicitSafeAreaEdges, GetDefaultSafeAreaEdges(). |
| — | New internal ISafeAreaInsets { Thickness SafeAreaInsets { set; } } (implemented only by Page). |
| — | New internal ISafeAreaViewStrategy { GetSafeAreaRegionsForEdge(int) } + internal static SafeAreaViewStrategy resolver. |
internal static SafeAreaElement (Controls) |
Now public, exposing SafeAreaEdgesProperty and IsSafeAreaEdgesSet(BindableObject). |
| — | New public static SafeAreaElementExtensions.GetEffectiveSafeAreaEdges(this ISafeAreaElement). |
Behavioural changes (not merely mechanical):
ViewHandler.cs:81— theSafeAreaEdgesmapper registration moved from#if ANDROID || IOSto#if ANDROID || IOS || MACCATALYST.MACCATALYSTis a distinct symbol fromIOSin this repo (confirmed by the pervasive#if IOS || MACCATALYSTidiom, e.g.ContentPage.cs), so the mapper was previously never registered on Mac Catalyst — runtimeSafeAreaEdgeschanges did not propagate there at all. This is a genuine, previously-unfixed bug.- Specificity-aware handler updates —
BindableProperty.UpdateHandlerOnSpecificityChange(new internal flag, set only forSafeAreaEdgesProperty), plumbed throughBindableObject.OnBindablePropertySet(newspecificityChangedparameter) intoElement.OnBindablePropertySet. Assigning the same value at a different specificity now updates the handler, because explicitness itself changes resolution. - Ancestor suppression became per-edge and value-based —
bool IsParentHandlingSafeArea()→SafeAreaEdges GetParentHandledSafeAreaEdges(), which recomputes each ancestor's adjusted insets rather than reading a cached bool, andExcludeParentHandledSafeAreaEdgeszeroes only overlapping edges. - Keyboard geometry rewritten —
TryGetSoftInputBottomOverlapnow converts the keyboard frame into window coordinates viawindow.ConvertRectFromCoordinateSpace(..., window.Screen.CoordinateSpace)and intersects against the view's own window-space frame (the old code compared_keyboardFrameagainstwindow.Frameand usedSuperview.ConvertRectToView(Frame, Window)). The!IsSoftInputHandledByParent(this)gate was removed and replaced by per-view frame-relative overlap plusbottomIncludesKeyboardOverlap. - New subtree invalidation —
MauiView.InvalidateSafeArea(UIView)(static, recursive over all subviews) andInvalidateDescendantSafeAreas(), fired from keyboard callbacks and fromMapSafeAreaEdges.
2. Independent assessment
The contract split is right. The old ISafeAreaView2 conflated three unrelated concerns — a per-edge resolution strategy, an inset write-back sink, and a public configuration surface — and forced every implementer (Border, ContentView, Layout, ScrollView, Page) to stub out the parts it did not need (Thickness ISafeAreaView2.SafeAreaInsets { set { } } appears four times in the deleted code). Splitting into ISafeAreaElement (public config) / ISafeAreaViewStrategy (internal resolution) / ISafeAreaInsets (internal sink) is correct layer placement, and centralising resolution in SafeAreaViewStrategy removes five near-duplicate GetSafeAreaRegionsForEdge implementations. The new types correctly live in src/Core so ISafeAreaElement is referenceable without adding Core interface deps to Controls.csproj — this matches the guidance in safe-area-ios.instructions.md.
Ordering in SafeAreaViewStrategy.TryGetSafeAreaEdges is ISafeAreaViewStrategy → ISafeAreaElement → (optionally) legacy ISafeAreaView, which preserves built-in compatibility behaviour while letting custom views opt into the modern contract. That is consistent across all four call sites, and includeLegacy: false is passed everywhere a legacy fallback would have changed existing behaviour.
The mechanical parts I verified as behaviour-preserving:
SafeAreaPaddingis(Left, Right, Top, Bottom); every new construction site passes them in that order (ExcludeParentHandledSafeAreaEdges,GetAdjustedSafeAreaInsets).SafeAreaEdgesis(Left, Top, Right, Bottom); every new construction site matches. No positional-argument transposition.SetterSpecificitydefinesoperator !=(SetterSpecificity.cs:248), andoriginalSpecificityis captured beforecontext.Values.SetValue(...)inSetValueActual, sospecificityChangedis computed correctly.SetterSpecificityList.GetSpecificity()is O(1) (_top.Specificity), so the new per-set computation is negligible.MauiWindowInsetListener.HasExplicitSafeAreaEdgesandSafeAreaExtensions.ApplyAdjustedSafeAreaInsetsPxretain equivalent type coverage after the swap (PageimplementsISafeAreaViewStrategy;ContentPageimplementsISafeAreaElement; both resolve to the same values as before).MauiScrollViewis aUIScrollView, not aMauiView, so theif (MauiView) … else if (MauiScrollView)dispatch inInvalidateSafeAreais correct, not an accidental exclusive branch.ViewHandler.iOS.MapSafeAreaEdgesuseshandler.PlatformView is PlatformView platformView, which null-guards via the pattern; no NRE risk there.
Where I disagree with the change as submitted is concentrated in the iOS runtime behaviour, not the contract. Three items are load-bearing and, in my reading, regressions or unproven assumptions; the rest are cost/documentation concerns. Details in §3.
3. Findings with evidence
3.1 [major] — MauiScrollView lost ancestor suppression on the SystemAdjustedContentInset branch
MauiScrollView.cs:389:
_appliesSafeAreaAdjustments = RespondsToSafeArea() && !_safeArea.IsEmpty;The !IsParentHandlingSafeArea() term was removed. Its replacement, ExcludeParentHandledSafeAreaEdges(_safeArea, GetParentHandledSafeAreaEdges()), is applied only inside the first branch (SystemAdjustedContentInset == UIEdgeInsets.Zero || ContentInsetAdjustmentBehavior == Never). The else branch is:
else
{
// UIKit's adjusted inset is authoritative once the scroll view is scrollable.
// Filtering it through MAUI ancestor padding would discard native scroll insets.
_safeArea = SystemAdjustedContentInset.ToSafeAreaInsets();
}No ancestor filtering, and _appliesSafeAreaAdjustments no longer gates on ancestors either. So for a scrollable ScrollView under a safe-area-applying MauiView ancestor, both now inset the same edges. Prior to this commit IsParentHandlingSafeArea() forced _appliesSafeAreaAdjustments = false in exactly that configuration.
Repro shape: ContentPage SafeAreaEdges="All" → VerticalStackLayout → ScrollView with content taller than the viewport (behavior stays Automatic, SystemAdjustedContentInset != Zero). This is the double-apply/oscillation family tracked by #33595 and #32586, and the comment justifying the carve-out ("UIKit's adjusted inset is authoritative") explains why UIKit's value should not be filtered, but does not address why the scroll view should still apply it when an ancestor already did.
Coverage: ScrollViewHandlerTests.iOS.cs adds exactly one test, LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge, which asserts ContentInsetAdjustmentBehavior == Never — i.e. it exercises the other branch. The regressed path has no test.
3.2 [major] — bottomIncludesKeyboardOverlap can permanently double keyboard padding
MauiView.cs:592:
!bottomIncludesKeyboardOverlap && parentHandledEdges.Bottom != SafeAreaRegions.None ? 0 : safeArea.Bottomwith the comment "Keep a child's positive frame-relative keyboard overlap until its parent arranges that child above the keyboard; the overlap is then zero and normal suppression applies."
That convergence argument holds only when the parent's bottom inset actually moves the child. It does not for a child that is bottom-aligned in a fixed-height row, absolutely positioned, or otherwise not repositioned by the parent's content-rect shrink. In those cases TryGetSoftInputBottomOverlap keeps returning a positive overlap indefinitely and both ancestor and descendant pad for the keyboard — roughly 2× keyboard height. The removed !IsSoftInputHandledByParent(this) gate is precisely what previously prevented this.
IsSoftInputHandledByParent still exists but is now only consumed by KeyboardAutoManagerScroll.AdjustPositionDebounce (KeyboardAutoManagerScroll.cs:309) — it no longer participates in inset computation at all.
3.3 [major] — layout-pass amplification and non-convergence risk
MauiView.cs:863 calls InvalidateDescendantSafeAreas() from inside ValidateSafeArea(), which runs on the layout path. Each descendant gets SetNeedsLayout() during a layout pass, and each descendant that changes then invalidates its own descendants.
Combined with §3.2's deliberate two-phase convergence, a single keyboard show on nested SoftInput edges costs a minimum of three passes (parent pads → child overlap resolves to 0 → parent re-measures). The EqualsAtPixelLevel guard in the return value (MauiView.cs:~866) terminates the loop only when values converge; it does not prevent a feedback cycle where the child's applied inset feeds back into the parent's arrange.
Related cost, same mechanism:
MauiView.cs:554—GetParentHandledSafeAreaEdgesnow callsmauiView.GetAdjustedSafeAreaInsets(...)per ancestor (interface type tests + virtualGetDefaultSafeAreaEdges()+ struct construction + a 4-edge scan, plusConvertRectToView/ConvertRectFromCoordinateSpacefor the nearest ancestor). Previously: read a cachedboolfield. O(views × depth), and the_parentHandledSafeAreaEdgescache is nulled for every descendant on each change.MauiView.cs:976—InvalidateSafeArea(UIView)recurses the entire native subtree with no early exit; each node'sSubviewsaccess marshals a fresh managed array. Fires on every keyboard show/hide/frame-change and on everyMapSafeAreaEdges.ViewHandler.iOS.cs:167— that full-subtree walk is now also triggered by specificity-only changes (viaUpdateHandlerOnSpecificityChange), e.g. aStylere-applying the sameSafeAreaEdgesvalue or aVisualStatere-entry.
Per repo convention, cache/hot-path changes of this shape need dotnet-trace/speedscope evidence; none is present in the diff.
3.4 [moderate] — ancestor region semantics are collapsed to a marker
MauiView.cs:567 and neighbours set left/top/right/bottom = SafeAreaRegions.Container purely as a "handled" marker, discarding the ancestor's actual region. A SoftInput-bottom ancestor (transient, keyboard-driven) therefore suppresses a Container-bottom child (persistent, home-indicator) for the duration of the keyboard session. Recording the ancestor's resolved region via SafeAreaViewStrategy.GetSafeAreaRegionsForEdge would let suppression apply only to genuinely overlapping semantics.
3.5 [moderate] — HasSoftInputBottomOverlapChanged() is a mutating predicate on the fast path
MauiView.cs:407. It mutates _lastSoftInputBottomOverlap, is non-idempotent within a pass, and runs before the !_safeAreaInvalidated early-return — so the steady-state "nothing changed" path now always pays a TryGetSafeAreaEdges call and, with the keyboard up, two coordinate conversions per layout.
3.6 [moderate] — bindable-property specificity coverage gap
Element.cs:711. The forward direction (setting a value at a new specificity) is covered. The reverse — ClearValue/style-unapply causing HasExplicitSafeAreaEdges to flip back to false — depends on the new original.Key != bpcontext.Values.GetSpecificity() check at BindableObject.cs:152, and SafeAreaTests.cs (+517 lines) contains no test exercising it. Note also that SetValueActual's if (specificity < originalSpecificity) branch returns without calling OnBindablePropertySet at all (pre-existing), so the specificity-change signal is deliberately partial.
3.7 [moderate] / [minor] — public-surface polish
SafeAreaElementExtensions.GetEffectiveSafeAreaEdges(ISafeAreaElement.cs:61) is documented as returning "the per-edge safe area strategy consumed by MAUI platform handlers", but handlers layer keyboard state and ancestor suppression on top of that value, so it does not describe what is applied. It also has no in-repo consumer.SafeAreaElement.IsSafeAreaEdgesSet(SafeAreaElement.cs:57) silently returnsfalsefor aBindableObjectthat declares its ownSafeAreaEdgesproperty rather than reusing the shared instance — a silent degradation in a newly public extensibility contract.Page.cs:268–271's newthis is ISafeAreaElementbranch is unreachable for every built-inPagesubclass (ContentPagere-implements the interface explicitly; all others never implementISafeAreaElement). It exists only for user subclasses and has no test.
4. Public API and compatibility assessment
Additions are correctly declared. PublicAPI.Unshipped.txt was updated for all 7 Controls RIDs and all 8 Core RIDs. Core entries:
Microsoft.Maui.ISafeAreaElement
Microsoft.Maui.ISafeAreaElement.SafeAreaEdges.get -> Microsoft.Maui.SafeAreaEdges
Microsoft.Maui.ISafeAreaElement.HasExplicitSafeAreaEdges.get -> bool
Microsoft.Maui.ISafeAreaElement.GetDefaultSafeAreaEdges() -> Microsoft.Maui.SafeAreaEdges
Microsoft.Maui.SafeAreaElementExtensions
static Microsoft.Maui.SafeAreaElementExtensions.GetEffectiveSafeAreaEdges(...) -> Microsoft.Maui.SafeAreaEdges
Controls entries add SafeAreaElement, SafeAreaElement.IsSafeAreaEdgesSet, SafeAreaElement.SafeAreaEdgesProperty. These match the actual shapes. PublicAPI.Shipped.txt was not touched — correct.
No breaking change to shipped API. ISafeAreaView2 and the old ISafeAreaElement were both internal, so their deletion/reshaping is invisible to consumers. SafeAreaEdgesDefaultValueCreator() → GetDefaultSafeAreaEdges() is an internal-to-public rename with no shipped predecessor. The public instance SafeAreaEdges properties on Border/ContentView/Layout/ScrollView/ContentPage already existed at the base commit and are unchanged.
Forward-compatibility risk to flag for the API council, not a blocker: ISafeAreaElement is now a public interface with three members. Adding a member to it later is a breaking change (default interface method / IFoo2 / extension method would be required). Given that the design explicitly justifies why all three members are needed, this is defensible — but SafeAreaElementExtensions.GetEffectiveSafeAreaEdges (§3.7) is the piece I would not ship without a demonstrated consumer, since public static extensions on a public contract cannot be withdrawn.
Behavioural compatibility: the Mac Catalyst mapper registration change (#if ANDROID || IOS → + || MACCATALYST) is a behaviour change on Mac Catalyst — runtime SafeAreaEdges mutations now take effect where they previously silently did nothing. That is the intended fix, but Mac Catalyst apps that inadvertently depended on the property being inert will see layout shifts. Worth a release note. Mac Catalyst also defaults UseSafeArea to true (unlike iOS), and ContentPage's new explicit ISafeAreaElement.SafeAreaEdges getter routes the unset case through ((ISafeAreaView)this).IgnoreSafeArea under #if IOS || MACCATALYST, which preserves that asymmetry correctly.
Default-value semantics for third parties: a custom view implementing only ISafeAreaElement and returning SafeAreaEdges.Default from GetDefaultSafeAreaEdges() resolves to Container (SafeAreaViewStrategy.ResolveDefaultRegion), which differs from every built-in control's default (Border/ContentView/ContentPage = None, Layout = Container, ScrollView = unresolved Default). This is documented in the interface remarks and appears deliberate, but it is a divergence third-party authors will hit.
5. Blast radius
High. This is not a localised fix.
- Types whose interface list changed:
Border,ContentPage,ContentView,Layout(base of every layout panel),Page(base of every page),ScrollView.LayoutandPagesit under essentially all MAUI content. - Framework-wide plumbing:
BindableObject.OnBindablePropertySetgained a parameter, overridden inElementandInputView.Element.OnBindablePropertySetruns for every bindable property set on every element — thespecificityChangedcomputation is now unconditional there. It is O(1), but the code path is the single hottest one in Controls. - iOS/Mac Catalyst layout core:
MauiView(+354/-… lines) andMauiScrollVieware the base platform views for nearly all MAUI content on Apple platforms. The safe-area/keyboard changes affect every page, every scroll view, and everyCollectionViewcell backed by aMauiView. - Android:
ViewHandler.Android.csgained anIsModernSafeAreaViewearly-return inMapSafeAreaEdges, andSafeAreaExtensions/MauiWindowInsetListenerwere re-pointed at the new resolver. Coverage looks equivalent, but the inset pipeline is shared by all Android views. - Platforms not exercised by the new tests: Windows and Tizen get new
PublicAPI.Unshipped.txtentries (the interfaces are in the sharednetstandard/netsurface) but no behavioural coverage — acceptable, since the mapper is#if ANDROID || IOS || MACCATALYST. - AOT/trimming: no reflection, no
Type.GetType/Activator.CreateInstance, no new suppressions orDynamicallyAccessedMembers.SafeAreaViewStrategyresolves via plainis-pattern type tests, which are trimmer- and AOT-safe. The.aotprofilebinaries were regenerated (small deltas, consistent with the removedISafeAreaView2dispatch). No AOT/trimming concerns. - XAML:
SafeAreaEdgesTests.xaml/.xaml.csgained coverage; the sharedSafeAreaEdgesPropertyis still surfaced per-type through each control's own public static field, so XAML addressability is preserved (the interface remarks explicitly call this out).
6. Hard failure-mode probes
| # | Probe | Result |
|---|---|---|
| P1 | Positional-arg transposition in the new SafeAreaPadding(L,R,T,B) / SafeAreaEdges(L,T,R,B) construction sites |
Pass — all four new sites match their declared orders; UIEdgeInsets(top,left,bottom,right) in GetInset also correct. |
| P2 | Does #if ANDROID || IOS || MACCATALYST change anything, or is IOS already implied on Mac Catalyst? |
Real change — #if IOS || MACCATALYST is used throughout this repo, proving MACCATALYST is disjoint. Previously-dead mapper on Mac Catalyst is now live. |
| P3 | SetterSpecificity != operator exists; originalSpecificity captured before mutation |
Pass — SetterSpecificity.cs:248; capture precedes context.Values.SetValue in SetValueActual. |
| P4 | Is GetSpecificity() O(1)? (new call on the universal BP-set path) |
Pass — SetterSpecificityList.GetSpecificity() returns _top.Specificity. |
| P5 | Guard made more restrictive — IsModernSafeAreaView early-return in MapSafeAreaEdges (both platforms). What previously-passing input does it now reject? |
Intentional — rejects views with a SafeAreaEdges-named property that implement neither ISafeAreaViewStrategy nor ISafeAreaElement. No such built-in exists; the new LegacySafeAreaViewWithoutModernContractRemainsEdgeToEdge test pins the legacy-only case. |
| P6 | Does a legacy-only ISafeAreaView still resolve? includeLegacy: false is passed at every new call site. |
Pass, by design — inset computation deliberately excludes legacy; legacy is preserved via ContentPage's compatibility getter and Page's strategy. |
| P7 | Ancestor suppression correctness when an intermediate ancestor's own inset is itself suppressed | Pass — GetParentHandledSafeAreaEdges reads the ancestor's raw adjusted insets, so an edge applied by a grandparent is still seen as handled by the intermediate; net effect is applied once. |
| P8 | MauiView vs MauiScrollView dispatch in InvalidateSafeArea — is the else if accidentally exclusive? |
Pass — MauiScrollView : UIScrollView, not a MauiView; branches are genuinely disjoint. |
| P9 | Upward layout loop from InvalidateDescendantSafeAreas |
Downward-only, terminates in isolation — but see §3.3 for the parent↔child keyboard feedback path that is not bounded by construction. |
| P10 | ExcludeParentHandledSafeAreaEdges reachable on the SystemAdjustedContentInset branch of MauiScrollView |
Fail — unreachable; see §3.1. |
| P11 | New negative-case test coverage for the new guards | Partial — P5's negative case is covered by the new stub test; §3.1's and §3.6's negative cases are not. |
| P12 | Input/path correctness (dimension 31) — external values reaching file/process/parser/navigation sinks | N/A — no such surface in this diff; no credentials, archives, paths, or URI parsing touched. |
7. Test and gate evidence status
Tests added (substantial, and largely well-targeted):
| File | Δ |
|---|---|
Core.UnitTests/SafeAreaTests.cs |
+517/−… (rewritten) |
DeviceTests/Elements/View/ViewTests.iOS.cs |
+1544 |
DeviceTests/Elements/View/ViewTests.Android.cs |
+173 |
DeviceTests/Elements/View/ViewTests.cs |
+35 |
DeviceTests/Elements/Page/PageTests.iOS.cs |
+18 |
Xaml.UnitTests/SafeAreaEdgesTests.xaml{,.cs} |
+20 |
Core/tests/DeviceTests/.../ScrollViewHandlerTests.iOS.cs |
+21 |
DeviceTests/CollectionView/CollectionViewTests.Android.cs |
+14/−… |
Test types are placed in the right projects (XAML tests in Xaml.UnitTests, handler tests in Core/tests/DeviceTests, control tests in Controls/tests/DeviceTests), and iOS device tests compile for Mac Catalyst, which gives incidental coverage of the P2 fix.
Identified coverage gaps (each maps to a finding):
- Scrollable
ScrollViewnested under a safe-area-applying ancestor — the branch regressed in §3.1. The one newScrollViewHandlerTests.iOS.cstest covers the opposite branch. - Nested
SoftInputbottom where the parent's inset does not reposition the child (§3.2). - Layout-pass count / non-oscillation assertion for a keyboard show over nested
SoftInputedges (§3.3). ClearValue/style-unapply onSafeAreaEdgesPropertyflippingHasExplicitSafeAreaEdgesback tofalse(§3.6).- A custom
Pagesubclass implementingISafeAreaElement, exercising the newPage.cs:268branch (§3.7). - No
dotnet-trace/speedscope evidence for the hot-path changes in §3.3, which repo convention requires for cache replacement.
Gate: the supplied gate is INCONCLUSIVE due to a build/environment error. Per the review instruction, this is explicitly not treated as a failing verification and does not by itself drive the verdict. Consequently I have no compile or test-execution evidence for this commit and this review is static-analysis-only; every finding above is derived from reading the diff, the full files at d7905a8af8, and the prior behaviour at ec79089f65. The claims most sensitive to that limitation are §3.2 and §3.3, whose failure modes are geometric and would be confirmed or refuted quickly on a device.
8. Verdict
NEEDS_CHANGES
Rationale. The architectural core of this PR — splitting ISafeAreaView2 into ISafeAreaElement / ISafeAreaViewStrategy / ISafeAreaInsets, centralising resolution in SafeAreaViewStrategy, and making the configuration contract public — is well-designed, correctly layered, properly declared in PublicAPI.Unshipped.txt, AOT/trim-clean, and backed by a genuinely large test addition. The Mac Catalyst mapper registration is a real bug fix. I would be comfortable with all of that.
The verdict is driven by one concrete regression and one unproven convergence assumption, both in iOS runtime geometry, both in code paths that sit under essentially every MAUI page:
- §3.1 (blocking) —
MauiScrollView.cs:389drops ancestor suppression on theSystemAdjustedContentInsetbranch, where the replacementExcludeParentHandledSafeAreaEdgescall is unreachable. This re-opens the double-apply configuration behind #33595/#32586 for the most common scroll case (content taller than viewport), and the single new scroll-view test covers the other branch. This needs either the exclusion applied to that branch or the gate restored, plus a regression test named against the issue. - §3.2 (blocking) — removing
!IsSoftInputHandledByParent(this)from inset computation in favour ofbottomIncludesKeyboardOverlapis only self-correcting when the parent's inset repositions the child. A bottom-aligned or absolutely-positionedSoftInputchild yields permanent double keyboard padding. This needs either a bound on the fallback or a device test proving convergence for a child the parent does not move. - §3.3 (needs evidence, not necessarily a code change) — the ancestor walk changed from reading a cached bool to recomputing each ancestor's full adjusted insets, and safe-area invalidation now recurses whole native subtrees on every keyboard event and every
MapSafeAreaEdges(including specificity-only no-op updates). Per repo convention for cache replacement on a hot path, this needsdotnet-tracenumbers before merge.
§3.4–§3.7 are non-blocking and can be addressed in follow-up, with one exception I would like resolved before merge because it cannot be withdrawn later: SafeAreaElementExtensions.GetEffectiveSafeAreaEdges should either have its documentation corrected or be dropped, since it is a new public static on a new public interface with no in-repo consumer and a doc comment that overstates what it returns.
Because the gate is inconclusive rather than red, none of the above rests on a failed build; all of it rests on the diff itself. If §3.1 and §3.2 are shown by device testing to be non-issues (i.e. the else branch is genuinely unreachable in practice for nested scroll views, and the overlap always converges), this moves to LGTM pending only the §3.3 trace and the §3.7 API note.
Confidence: high on §3.1 (the unreachable-guard argument is purely structural and does not depend on runtime behaviour); medium-high on §3.2 and §3.3 (the failure geometry is clear but unverified on device, as the gate could not run).
🛠️ Try-Fix — Analysis & Comparison
PR #37750 — STEP 5a Try-Fix Aggregate
Candidate 1 — Handler-Mapped Attached Safe-Area State
Model: claude-opus-5
Result: Blocked
Files changed: None
Self-review: 0 findings
Candidate narrative: ../try-fix-1/content.md
Attempt artifacts: attempt-1/
Approach
Expose safe-area configuration as an attached Controls property on any VisualElement, carry it through the existing ViewHandler property mapper, and push the resolved four-edge value onto MauiView/MauiScrollView as host-owned state. Keep the shipped ISafeAreaView fallback unchanged.
This avoids PR #37750's public ISafeAreaElement plus centralized strategy-resolver design. The PR fixes an unrecognized custom view by broadening a host-side type-identity pull; candidate 1 instead removes the recognition step. A handler mapper pushes data to the native host, so participation depends on the existing handler pipeline rather than implementation of a new MAUI interface.
Result and Failure Analysis
The approach was designed but not implemented. EstablishBrokenBaseline.ps1 rejected the pre-existing dirty worktree before creating .github/.baseline-state.json, so no RevertedFiles edit allow-list existed. The unrelated .github/scripts, .github/skills, and eng/scripts changes are off-limits and cannot be cleaned by an attempt. Independently, the PR adds ISafeAreaInsets.cs and ISafeAreaViewStrategy.cs; a successfully generated baseline would therefore contain NewFiles, which also requires Blocked.
The candidate diff is empty. The primary SafeAreaEdges HostApp command was skipped because no fix could be applied. All 13 supplied regression commands were consequently skipped because they are gated on primary-test success. The required restore command ran and reported the expected no-state result: No baseline state found / Restored False; no attempt-created changes existed.
Candidate 2 must avoid the attached-property, handler-mapper push, and native-host-owned strategy mechanism.
Candidate 2 — Version Existing ISafeAreaView
Model: gpt-5.6-sol
Result: Blocked
Files changed: None
Self-review: 0 findings
Candidate narrative: ../try-fix-2/content.md
Attempt artifacts: attempt-2/
Approach
Extend the already-public, already-recognized ISafeAreaView with a getter-only per-edge strategy and default interface behavior. Existing implementations would map IgnoreSafeArea to all-None or all-Container; custom views could override the per-edge member through a concrete bindable/XAML property. Apple hosts would continue pulling through the existing contract identity.
This differs from both earlier mechanisms. It neither introduces PR #37750's second public contract and centralized compatibility resolver nor uses candidate 1's attached property and handler-mapper push. It addresses missing expressiveness by versioning the public contract native hosts already recognize, with default interface members providing legacy compatibility.
Result and Failure Analysis
The approach was designed but not implemented. Baseline setup again rejected the pre-existing dirty tracked CI/script files and left .github/.baseline-state.json absent, so there was no RevertedFiles edit allow-list. The PR's added production files would independently force Blocked through the NewFiles rule.
The candidate diff is empty. The primary SafeAreaEdges HostApp command was skipped because no implementation could legally be applied. All 13 supplied regression commands were skipped because the primary did not pass. The required restore command ran and reported the expected no-state result: No baseline state found / Restored False; no attempt-created changes existed.
Aggregate Outcome
Two distinct alternative mechanisms were produced, satisfying the two-candidate cap, but neither could be implemented or empirically evaluated under the enforced baseline boundary. Both results are Blocked, not Fail: no compile, runtime, simulator, primary-test, or regression-test evidence was obtained. STEP 5b should assess the current PR code on its merits without treating either blocked candidate or the inconclusive gate as evidence against it.
🏁 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Winner: pr
The raw submitted PR is the comparative winner because it is the only implemented candidate with a passing required regression record: all 13 issue-specific iOS regressions passed. This is not an approval. The trusted Gate remains inconclusive, the expert evaluation identified unresolved Apple-platform correctness/performance concerns, and the functionally equivalent pr-plus-reviewer runtime path failed one focused SafeAreaEdges test.
Comparative ranking
| Rank | Candidate | Implementation | Validation evidence | Assessment |
|---|---|---|---|---|
| 1 | pr |
Complete submitted 56-file public-contract and platform resolver implementation | Gate inconclusive; 13/13 required issue regressions passed | Best available evidence and therefore the winner, but not ready for approval while expert concerns and the runtime-change failure remain unresolved. |
| 2 | pr-plus-reviewer |
Raw PR plus corrected public docs, explicit mutating-helper naming, and ClearValue specificity coverage |
HostApp built; 11/12 SafeAreaEdges tests passed, with VerifyRuntimeSafeAreaEdgesChange timing out |
Improves API clarity and coverage, but ranks below the regression-passing raw PR because its required targeted validation failed. No retry was performed. |
| 3 | try-fix-1 |
Proposed attached SafeArea.Edges state pushed through the handler mapper |
Blocked before implementation; empty diff; no tests | Design-only candidate with no empirical evidence. |
| 4 | try-fix-2 |
Proposed versioning of the existing ISafeAreaView contract |
Blocked before implementation; empty diff; no tests | Design-only candidate with no empirical evidence. |
Expert review reconciliation
The expert pass correctly identified three low-risk improvements: the public GetEffectiveSafeAreaEdges() documentation described final handler behavior too strongly, the keyboard-overlap helper name hid mutation, and the reverse ClearValue specificity transition lacked a focused test. These are captured in pr-plus-reviewer/reviewer.patch.
Two proposed runtime reversions were not safe to apply in the single refinement:
SystemAdjustedScrollViewInsetsAreNotSuppressedByParentexplicitly establishes that UIKit-adjusted scroll insets remain authoritative when an ancestor has a safe area. Applying ancestor filtering to that branch would reverse submitted behavior without a reproducer.ParentAndChildKeyboardSafeAreasProtectOverflowingChildexplicitly requires a non-repositioned child to retain its frame-relative keyboard overlap, whileParentAndChildKeyboardSafeAreasDoNotDoublePadArrangedChildverifies suppression after the child is repositioned. Restoring an all-or-nothing parent gate would regress the overflowing-child case.
The expert's hot-path concern remains unresolved: recursive descendant invalidation and per-ancestor adjusted-inset resolution have broad layout cost, and no trace comparison is available. More importantly, the one-shot candidate validation exposed a direct runtime failure in VerifyRuntimeSafeAreaEdgesChange; because the candidate's runtime code differs only by a private rename, that result creates uncertainty about the submitted behavior rather than demonstrating a reviewer-patch regression.
Required disposition
Request changes to resolve or explain the VerifyRuntimeSafeAreaEdgesChange failure and provide evidence for the unresolved Apple safe-area propagation/performance concerns. The inconclusive Gate is recorded as uncertainty and is not, by itself, the reason for this recommendation.
🔗 Regression Cross-Reference
🔍 Regression Cross-Reference
✗ Revert risks detected — this PR removes 3 line(s) previously added by labeled bug-fix PRs.
| File | Fix PR | Fixed issue(s) | Risk | Reverted line |
|---|---|---|---|---|
src/Core/src/Platform/iOS/MauiScrollView.cs |
#34024 | #32586, #33934, #33595, #34042 | ✗ REVERT | bool? _parentHandlesSafeArea; |
src/Core/src/Platform/iOS/MauiView.cs |
#34024 | #32586, #33934, #33595, #34042 | ✗ REVERT | bool? _parentHandlesSafeArea; |
src/Core/src/Platform/iOS/SafeAreaPadding.cs |
#34024 | #32586, #33934, #33595, #34042 | ✗ REVERT | return RoundToPixel(Left, scale) == RoundToPixel(other.Left, scale) |
Action required: Verify that issues #32586, #33595, #33934, #34042 do not re-regress before merging.
🧪 Regression Tests to Verify
These tests were added by the fix PRs being reverted. They must still pass:
| Fix PR | Type | Test | Filter |
|---|---|---|---|
| #34024 | UITest | Issue28986_ParentChildTest | Issue28986_ParentChildTest |
| #34024 | UITest | Issue32586 | Issue32586 |
| #34024 | UITest | Issue33595 | Issue33595 |
| #34024 | UITest | Issue33934 | Issue33934 |
| #34024 | UITest | Issue28986_ParentChildTest | Issue28986_ParentChildTest |
| #34024 | UITest | Issue32586 | Issue32586 |
| #34024 | UITest | Issue33595 | Issue33595 |
| #34024 | UITest | Issue33934 | Issue33934 |
| #34024 | UITest | Issue28986_ParentChildTest | Issue28986_ParentChildTest |
| #34024 | UITest | Issue32586 | Issue32586 |
| #34024 | UITest | Issue33595 | Issue33595 |
| #34024 | UITest | Issue33934 | Issue33934 |
🧪 Regression Test Results
✅ PASSED — 13 passed, 0 failed, 0 skipped
| Fix PR | Test | Type | Result |
|---|---|---|---|
| #35916 | Issue35756 | UITest | ✅ PASSED |
| #34024 | Issue28986_ParentChildTest | UITest | ✅ PASSED |
| #34024 | Issue32586 | UITest | ✅ PASSED |
| #34024 | Issue33595 | UITest | ✅ PASSED |
| #34024 | Issue33934 | UITest | ✅ PASSED |
| #34024 | Issue28986_ParentChildTest | UITest | ✅ PASSED |
| #34024 | Issue32586 | UITest | ✅ PASSED |
| #34024 | Issue33595 | UITest | ✅ PASSED |
| #34024 | Issue33934 | UITest | ✅ PASSED |
| #34024 | Issue28986_ParentChildTest | UITest | ✅ PASSED |
| #34024 | Issue32586 | UITest | ✅ PASSED |
| #34024 | Issue33595 | UITest | ✅ PASSED |
| #34024 | Issue33934 | UITest | ✅ PASSED |
📱 UI Tests — Border,Layout,Page,SafeAreaEdges,ScrollView,ViewBaseTests
Detected UI test categories: Border,Layout,Page,SafeAreaEdges,ScrollView,ViewBaseTests
✅ Deep UI tests — 661 passed, 0 failed, 6 skipped across 6 categories on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Border |
58/58 ✓ | — |
Layout |
194/199 (5 skipped) ✓ | — |
Page |
26/26 ✓ | — |
SafeAreaEdges |
109/109 ✓ | — |
ScrollView |
162/163 (1 skipped) ✓ | — |
ViewBaseTests |
112/112 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
🧭 Next Steps — review latest findings
No alternative fix was selected for this run. Review the session findings and CI results before merging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d00747b7-96f3-4e7a-8dfb-e3a48db04b2d
|
@MauiBot addressed the actionable review follow-ups in 01770ad: added mutation-proven |
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Summary
ISafeAreaElementthe public per-edge safe-area contract for custom views and native hostsHasExplicitSafeAreaEdgesandGetDefaultSafeAreaEdges()so platform code can preserve control defaults and distinguish an explicit value from a default-created valueGetEffectiveSafeAreaEdges()so public native hosts read the same effective strategy as MAUI handlers, including built-in controls that intentionally preserve an explicitDefaultSafeAreaElement.SafeAreaEdgesPropertyandSafeAreaElement.IsSafeAreaEdgesSet(BindableObject)helper for customBindableObjectimplementationsISafeAreaViewcontract unchanged as a compatible legacy fallbackThis targets
net11.0because #37384 is an API request for .NET 11.Public API and compatibility
The new public surface is added to every applicable Core and Controls API baseline:
ISafeAreaElement.SafeAreaEdgesISafeAreaElement.HasExplicitSafeAreaEdgesISafeAreaElement.GetDefaultSafeAreaEdges()SafeAreaElementExtensions.GetEffectiveSafeAreaEdges(ISafeAreaElement)SafeAreaElement.SafeAreaEdgesPropertySafeAreaElement.IsSafeAreaEdgesSet(BindableObject)ISafeAreaViewremains unchanged because it is already shipped and implemented by external controls. Built-in controls use an internal strategy for their existing legacy/default semantics; third-party controls only implementISafeAreaElement.Leaving
ContentPage.SafeAreaEdgesunset preserves its existing platform/legacy default. Explicit values remain explicit, including full and partialDefaultregions, so setting the shipped named default does not become edge-to-edge.GetEffectiveSafeAreaEdges()delegates to the same strategy resolver as the handlers, preventing public native hosts from interpreting those values differently.Validation
The broad focused build, API, unit, XAML, AOT, and platform validation below was completed through product head
777f65e227af9af1a0148a55223317a59ec94a0a. Commitsc32e41e882e0b6baa9da3b830a2840262e015c2eand310d7e93c0fd3280aba2833ef1191b54afd51a41harden nested BottomSoftInputresidual handling and its geometry-sensitive coverage. Commitc265a307d821eea36c61116b1a549fdf7a9e03f6adds pixel-gated overlap caching, screen-to-window keyboard conversion, active-ancestor checks, descendant convergence invalidation, a real cross-platform arrange regression, and reusable bindable-property specificity metadata. Commit104f8146610f38465118de78bdd2c2f9336e4751compares absolute overlap values in device-pixel buckets so cumulative sub-pixel movement cannot be ignored indefinitely. Current PR head79cdc7408d6140335f5fa27a9aeb999897cfb0f9adds two-dimensional floating-keyboard intersection and clamping, uses the owning window's screen scale, preserves UIKit-applied scroll insets, verifies specificity-only/no-op updates, and removes avoidable ancestor work without making suppression depend on layout order. It includes target headbedd1b18b7682193e05b47267509cec8c49c6853, preserving both the target's initial-connection guard and this PR's dynamic safe-area invalidation.The screenshots and videos were captured at
f5a35e4a729a7875b26e2474e4fd90c1a1af0099. The later commits repair Android AOT profile encoding, clarify compatibility documentation, and harden nested keyboard behavior; they do not change the capturedContainer → None → bottom-only Container → Containerprobe flow. That flow and the keyboard regressions were rebuilt and revalidated in the final platform suites.SafeAreaTests:77/77SafeAreaTestsplus bindable-object selection at merged head:174/174SafeAreaEdgesTests:15/15across runtime, XamlC, and source-generator paths, including a third-party control using the public shared propertyMicrosoft.Maui.BuildTasks.slnf: succeeded withPublicApiType=Validatenetstandard2.0, iOS, Mac Catalyst, and AndroidThe platform-handler implementation was exercised at
f6db02a3ad6cb67b3e555085ca03eb33997f2b98; the only later commit adds the public resolver, its documentation/API baselines, and focused tests. Exact-head platform libraries and both empirical apps were rebuilt, and the new resolver itself is displayed in the iOS and Android evidence below.ViewPageScrollViewScrollViewHandler62/625/512passed,1ignored57passed,1ignored62/625/512passed,1ignored52passed,6ignoredView)53/5316/1612/1279cdc7408d6140335f5fa27a9aeb999897cfb0f9was rebuilt and empirically rerun after publication: iOS62/62, Mac Catalyst62/62, Android53/53, and focused units77/77FloatingKeyboardUsesClampedViewIntersectioncovers a laterally disjoint floating keyboard and an oversized frame; restoring the old Y-only overlap produced the sole iOS failure (61/62)SystemAdjustedScrollViewInsetsAreNotSuppressedByParentpreserves the full viewport inset that UIKit already applies; restoring system-inset suppression produced the sole iOS failure (61/62)60/62)61/62)_safeAreaInvalidatedsubtree early exit produced the sole iOS failure (61/62), proving that measure-invalidated parents can still have descendants requiring safe-area invalidation76/77)SoftInput: a correctly arranged child does not double-pad, while an overflowing child retains its positive frame-relative residual; transformed geometry is recomputed without requiring another keyboard notification, keyboard hide clears the residual, and a nestedMauiScrollViewstill suppresses its raw/system Bottom insetGridmeasure/arrange path instead of assigning native frames manuallySoftInputguard produced50/51, with only the overflowing-child regression failing50/51, with the transformed child retaining stale height50instead of the expected100MauiScrollView's raw/system Bottom inset as a computed keyboard residual produced50/51, with only the nested scroll-view suppression regression failing55/56, with only the stable-geometry cache regression failing (expected0ancestor reads, actual4)55/56, with only the strengthened cache regression failing because two individually sub-pixel moves crossed a cumulative device-pixel boundary55/56, with only the nonzero-window-origin regression failing (expected X0, actual137)SoftInputancestor underUIScrollViewas active produced55/56, with only the fallback-auto-scroll regression failing55/56, with only the non-SoftInputdescendant convergence regression failing (expected height70, actual100)51/53, failing exactly the listener attach/detach regressions; the restored APK passed53/53Invalid userId -2; the same rebuilt APK passed53/53when instrumentation explicitly targeted emulator owner user0SafeAreaEdgescases passed across final-head runs: both full-category runs passed108/109, with only the order-dependent initial-state assertion after an orientation-changing fixture failing; that complete fixture immediately passed4/4in isolation, and the earlier exact-f5afull run passed109/109OnBindablePropertySetentriesMicrosoft.Maui.SafeAreaViewStrategytype, one Android-reachableTryGetSafeAreaEdgesmethod, no leading-dot type, and no linker-unreachableGetSafeAreaRegionsForEdgeentrymaui: 38 modules / 1,814 types / 8,632 methods;maui-sc: 49 / 2,264 / 10,963;maui-blazor: 43 / 2,186 / 8,568110/110assemblies; the unstripped output contains the nativeMicrosoft_Maui_SafeAreaViewStrategy_TryGetSafeAreaEdges_object_Microsoft_Maui_SafeAreaEdges__boolbodyContentPage.Default, specificity-only assignments, Android exact inset consumption, nested/disjoint Apple edges, child keyboard overlap beyond a parent container inset, complete descendant invalidation, legacyMauiScrollViewbehavior, residual sub-pixel ancestor insets, completed ancestor traversal, and stale five-parameter AOT signaturesCompleted public build
1567759uses merge commit30ce3756e59cf4d10e60ea9c6ce9f5e916746767, whose second parent is PR head490a33f8dc17a653823d181f844aacd41b2e87fc. Every integration-test job succeeded, including Android runtime, six iOS CoreCLR/NativeAOT configurations, AOT/build, samples, Blazor, multi-project, and Windows template/build coverage.Replacement build
1567853uses merge commitcdb262e0517e81025f2038574b3a1bd9da829515, with target parent4695c95801e0b6764beb83f314c62141ee9c7f2eand PR parent310d7e93c0fd3280aba2833ef1191b54afd51a41. Its final raw timeline contains 24 successful jobs and only two failed jobs: Windows Debug and Release. Every failed task/job/phase log was downloaded and inspected without deduplication; both build tasks report only the target branch's fourCS0103errors for missingAssertEventuallyatTabbedPageTests.Windows.cslines 141 and 155 across the two Windows TFMs. Both Windows Helix jobs succeeded. Target commitc127c3f3503e06a347c0f100504911659f6154c1fixes that compile error and is included in current head79cdc7408d6140335f5fa27a9aeb999897cfb0f9.Build
1568885was audited from every failed raw phase/job/task log and all 16 Helix work-item details and console logs, preserving repeated lines. Every test process passed with zero failures and exited0; Windows Helix reported exit-4only when publishing results failed withTF10216: Azure DevOps services are currently unavailableor one Azure request-read timeout. This build contains no product or test failure.Claude Opus 5 independently reviewed the safe-area hot paths and rejected both applied-state ancestor caching and
_safeAreaInvalidatedsubtree pruning as layout-order correctness regressions. Its topology-independent optimizations are in79cdc7408d. A separate GPT-5.6 Terra exact-diff review reported no significant issues. All six exact-head review threads have evidence-backed replies and are resolved.Empirical evidence
A temporary
SafeAreaProbe : TemplatedView, ISafeAreaElementimplemented only the new public contract, reused the public shared bindable property, and displayedGetEffectiveSafeAreaEdges()asPublic host: .... At the capture head it was advanced live through all-edgeContainer,None, bottom-onlyContainer, and back to all-edgeContainer. The probe was removed after capture.Both videos are H.264/yuv420p at a constant 30 fps and contain only the ordered runtime states
1 → 2 → 3 → 1; sampled-frame review found no blank, splash, crash, or stale-state frames. On iOS, Appium taps drove the live property changes deterministically.iOS — iPhone 11 Pro simulator
T44/B34)T0/B0)T0/B34)Video — live runtime transitions:
ios-safe-area-demo-f5a.mp4
Android — API 34 arm64 emulator
The probe theme leaves the Android system bars opaque. Edge-to-edge is demonstrated by the
SAFE CONTENTmarkers moving beneath those bars (and therefore disappearing), while bottom-only restores only the bottom marker; the background cannot show through the opaque bars.Video — live runtime transitions:
android-safe-area-demo-f5a.mp4
MauiBot follow-up
Defaultresolution, explicit shared-property misuse, and the public CLR-property pattern required by XAMLContentPage.Defaultregions while retaining the unset edge-to-edge defaultSoftInputMauiViewcompute its own residual even when an ancestor also declaresSoftInput: an arranged child falls back to ordinary ancestor suppression, while an overflowing child retains only its positive live overlapSoftInputgeometry during layout so transforms cannot preserve stale keyboard overlap; mutation testing makes the transformed-child assertion fail without the refreshSoftInputancestor to respond to safe area before it can suppress keyboard auto-scrollGridElementsafe-area identity special case with reusable internal bindable-property specificity metadataMauiScrollViewdeliberately keeps ordinary suppression for raw/system insets, with a bottom-edge regression that fails if the keyboard exemption is applied thereSystemAdjustedContentInsetvalues while keeping per-edge ancestor suppression for manually computed scroll insets_safeAreaInvalidatedcannot represent descendant state; a persistent latch is also unsound across same-window reparenting of genericUIViewsubtreesIScrollViewimplementationsExisting PR comparison
I searched the open pull requests for #37384 and equivalent safe-area API titles. No competing implementation exists, so there was no alternative change set to compare.
Fixes #37384